/*
 * Unidata Platform Community Edition
 * Copyright (c) 2013-2020, UNIDATA LLC, All rights reserved.
 * This file is part of the Unidata Platform Community Edition software.
 *
 * Unidata Platform Community Edition is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * Unidata Platform Community Edition is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
 */
package org.unidata.mdm.data.type.merge;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;

import org.apache.commons.collections4.MapUtils;
import org.apache.commons.lang3.StringUtils;
import org.unidata.mdm.core.type.timeline.Timeline;
import org.unidata.mdm.data.type.data.OriginRelation;
import org.unidata.mdm.data.type.keys.RelationKeys;

/**
 * Utility type, which helps at relation merge.
 * @author Mikhail Mikhailov on May 5, 2020
 */
public class MergeRelationMasterState implements MergeMasterState {
    /**
     * Master from timelines state.
     */
    private final Map<String, List<Timeline<OriginRelation>>> fromTimelines;
    /**
     * Master to timelines state.
     */
    private final Map<String, List<Timeline<OriginRelation>>> toTimelines;
    /**
     * From master ids.
     */
    private final Map<String, Map<String, RelationKeys>> fromIdsMap;
    /**
     * To master ids.
     */
    private final Map<String, Map<String, RelationKeys>> toIdsMap;
    /**
     * Constructor.
     * @param ctx the context
     */
    public MergeRelationMasterState(
            final Map<String, List<Timeline<OriginRelation>>> from,
            final Map<String, List<Timeline<OriginRelation>>> to) {

        super();
        this.fromTimelines = MapUtils.isEmpty(from) ? Collections.emptyMap() : from.values().stream()
                .flatMap(Collection::stream)
                .collect(Collectors.toMap(
                        t -> t.<RelationKeys>getKeys().getEtalonKey().getId(),
                        t -> new ArrayList<Timeline<OriginRelation>>(Collections.singleton(t))));

        this.toTimelines = MapUtils.isEmpty(to) ? Collections.emptyMap() : to.values().stream()
                .flatMap(Collection::stream)
                .collect(Collectors.toMap(
                        t -> t.<RelationKeys>getKeys().getEtalonKey().getId(),
                        t -> new ArrayList<Timeline<OriginRelation>>(Collections.singleton(t))));

        this.fromIdsMap = MapUtils.isEmpty(from) ? Collections.emptyMap() : new HashMap<>();
        this.toIdsMap = MapUtils.isEmpty(to) ? Collections.emptyMap() : new HashMap<>();

        // 1. Build from timelines digest
        from.forEach((key, value) -> value.forEach(t -> putFromMasterKey(t.getKeys())));

        // 2. Build to timelines digest
        to.forEach((key, value) -> value.forEach(t -> putToMasterKey(t.getKeys())));
    }
    /**
     * Gets existing _master_ relation key if master has already a relation to the same TO record.
     * Will return null for the same relation etalon id, assuming the caller wants other existing master keys
     * but not the keys being processed, since timeline executor updates the state.
     *
     * @param relationKeys the relation keys
     * @return existing keys or null
     */
    @Nullable
    public RelationKeys getMasterKeysByTypeAndToId(@Nonnull RelationKeys relationKeys) {

        Map<String, RelationKeys> masterKeysTable = fromIdsMap.get(relationKeys.getRelationName());
        if (Objects.nonNull(masterKeysTable)) {

            RelationKeys exisiting = masterKeysTable.get(relationKeys.getEtalonKey().getTo().getId());
            return Objects.nonNull(exisiting) && !StringUtils.equals(exisiting.getEtalonKey().getId(), relationKeys.getEtalonKey().getId())
                    ? exisiting
                    : null;
        }

        return null;
    }
    /**
     * Gets existing _master_ relation key if master has already a relation to the same FROM record.
     * Will return null for the same relation etalon id, assuming the caller wants other existing master keys
     * but not the keys being processed, since timeline executor updates the state.
     *
     * @param toId the to record id
     * @return existing keys or null
     */
    @Nullable
    public RelationKeys getMasterKeysByTypeAndFromId(@Nonnull RelationKeys relationKeys) {

        Map<String, RelationKeys> masterKeysTable = toIdsMap.get(relationKeys.getRelationName());
        if (Objects.nonNull(masterKeysTable)) {

            RelationKeys existing = masterKeysTable.get(relationKeys.getEtalonKey().getFrom().getId());
            return Objects.nonNull(existing) && !StringUtils.equals(existing.getEtalonKey().getId(), relationKeys.getEtalonKey().getId())
                    ? existing
                    : null;
        }

        return null;
    }

    public void putFromMasterKey(@Nonnull RelationKeys keys) {
        fromIdsMap.computeIfAbsent(keys.getRelationName(), k -> new HashMap<String, RelationKeys>())
                  .put(keys.getEtalonKey().getTo().getId(), keys);
    }

    public void putToMasterKey(@Nonnull RelationKeys keys) {
        toIdsMap.computeIfAbsent(keys.getRelationName(), k -> new HashMap<String, RelationKeys>())
                .put(keys.getEtalonKey().getFrom().getId(), keys);
    }

    public void putFromTimeline(@Nonnull Timeline<OriginRelation> timeline) {
        RelationKeys keys = timeline.getKeys();
        fromTimelines.computeIfAbsent(keys.getEtalonKey().getId(), k -> new ArrayList<Timeline<OriginRelation>>()).add(timeline);
        putFromMasterKey(keys);
    }

    public void putToTimeline(@Nonnull Timeline<OriginRelation> timeline) {
        RelationKeys keys = timeline.getKeys();
        toTimelines.computeIfAbsent(keys.getEtalonKey().getId(), k -> new ArrayList<Timeline<OriginRelation>>()).add(timeline);
        putToMasterKey(keys);
    }

    public Timeline<OriginRelation> getCurrentFromTimeline(@Nonnull RelationKeys keys) {
        List<Timeline<OriginRelation>> timelines = fromTimelines.get(keys.getEtalonKey().getId());
        return Objects.isNull(timelines) ? null : timelines.get(timelines.size() - 1);
    }

    public Timeline<OriginRelation> getCurrentToTimeline(@Nonnull RelationKeys keys) {
        List<Timeline<OriginRelation>> timelines = toTimelines.get(keys.getEtalonKey().getId());
        return Objects.isNull(timelines) ? null : timelines.get(timelines.size() - 1);
    }

    public Timeline<OriginRelation> getPreviousFromTimeline(@Nonnull RelationKeys keys) {
        List<Timeline<OriginRelation>> timelines = fromTimelines.get(keys.getEtalonKey().getId());
        return Objects.nonNull(timelines) && timelines.size() > 1 ? timelines.get(timelines.size() - 2) : null;
    }

    public Timeline<OriginRelation> getPreviousToTimeline(@Nonnull RelationKeys keys) {
        List<Timeline<OriginRelation>> timelines = toTimelines.get(keys.getEtalonKey().getId());
        return Objects.nonNull(timelines) && timelines.size() > 1 ? timelines.get(timelines.size() - 2) : null;
    }
}
